* @phpstan-var array */ private array $registered_connectors = array(); /** * Registers a new connector. * * Validates the provided arguments and stores the connector in the registry. * For connectors with `api_key` or `application_password` authentication, a * `setting_name` can be provided explicitly. When omitted, setting names are * automatically generated using the pattern `connectors_{$type}_{$id}_{$method}`, * with hyphens in the type and ID normalized to underscores. These setting * names are used for Settings API registration and REST API exposure. * * Registering a connector with an ID that is already registered will trigger a * `_doing_it_wrong()` notice and return `null`. To override an existing connector, * call `unregister()` first. * * @since 7.0.0 * * @see WP_Connector_Registry::unregister() * * @param string $id The unique connector identifier. Must match the pattern * `/^[a-z0-9_-]+$/` (lowercase alphanumeric, hyphens, and underscores only). * @param array $args { * An associative array of arguments for the connector. * * @type string $name Required. The connector's display name. * @type string $description Optional. The connector's description. Default empty string. * @type string $logo_url Optional. URL to the connector's logo image. * @type string $type Required. The connector type, e.g. 'ai_provider'. * @type array $authentication { * Required. Authentication configuration. * * @type string $method Required. The authentication method: 'api_key', * 'application_password', or 'none'. * @type string $credentials_url Optional. URL where users can obtain API credentials. * @type string $setting_name Optional. The setting name for the API key * or application-password credentials. When * omitted, auto-generated as * `connectors_{$type}_{$id}_api_key` for API * keys and `connectors_{$type}_{$id}_application_password` * for application passwords. * Must be a non-empty string when provided. * @type string $constant_name Optional. PHP constant name for the API key * (e.g. 'ANTHROPIC_API_KEY') or for application-password * credentials in `username:password` format. Only checked * when provided. * @type string $env_var_name Optional. Environment variable name for the API key * (e.g. 'ANTHROPIC_API_KEY') or for application-password * credentials in `username:password` format. Only checked * when provided. * } * @type array $plugin { * Optional. Plugin data for install/activate UI. * * @type string $file Optional. The plugin's main file path relative to the * plugins directory (e.g. 'my-plugin/my-plugin.php' or * 'hello.php'). * @type callable $is_active Optional callback to determine whether the plugin * is active. Receives no arguments and must return bool. * Defaults to `__return_true`. * } * } * @return array|null The registered connector data on success, null on failure. * * @phpstan-param array{ * name: non-empty-string, * description?: string, * logo_url?: non-empty-string, * type: non-empty-string, * authentication: array{ * method: 'api_key'|'application_password'|'none', * credentials_url?: non-empty-string, * setting_name?: non-empty-string, * constant_name?: non-empty-string, * env_var_name?: non-empty-string * }, * plugin?: array{ * file?: non-empty-string, * is_active?: callable(): bool * } * } $args * @phpstan-return Connector|null */ public function register( string $id, array $args ): ?array { if ( ! preg_match( '/^[a-z0-9_-]+$/', $id ) ) { _doing_it_wrong( __METHOD__, __( 'Connector ID must contain only lowercase alphanumeric characters, hyphens, and underscores.' ), '7.0.0' ); return null; } if ( $this->is_registered( $id ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" is already registered.' ), esc_html( $id ) ), '7.0.0' ); return null; } // Validate required fields. if ( empty( $args['name'] ) || ! is_string( $args['name'] ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" requires a non-empty "name" string.' ), esc_html( $id ) ), '7.0.0' ); return null; } if ( empty( $args['type'] ) || ! is_string( $args['type'] ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" requires a non-empty "type" string.' ), esc_html( $id ) ), '7.0.0' ); return null; } if ( ! isset( $args['authentication'] ) || ! is_array( $args['authentication'] ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" requires an "authentication" array.' ), esc_html( $id ) ), '7.0.0' ); return null; } if ( empty( $args['authentication']['method'] ) || ! in_array( $args['authentication']['method'], array( 'api_key', 'application_password', 'none' ), true ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" authentication method must be "api_key", "application_password", or "none".' ), esc_html( $id ) ), '7.0.0' ); return null; } if ( 'ai_provider' === $args['type'] && ! wp_supports_ai() ) { // No need for a `doing_it_wrong` as AI support is disabled intentionally. return null; } $connector = array( 'name' => $args['name'], 'description' => isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '', 'type' => $args['type'], 'authentication' => array( 'method' => $args['authentication']['method'], ), ); if ( ! empty( $args['logo_url'] ) && is_string( $args['logo_url'] ) ) { $connector['logo_url'] = $args['logo_url']; } $requires_credentials = in_array( $args['authentication']['method'], array( 'api_key', 'application_password' ), true ); if ( $requires_credentials ) { if ( ! empty( $args['authentication']['credentials_url'] ) && is_string( $args['authentication']['credentials_url'] ) ) { $connector['authentication']['credentials_url'] = $args['authentication']['credentials_url']; } if ( isset( $args['authentication']['setting_name'] ) ) { if ( ! is_string( $args['authentication']['setting_name'] ) || '' === $args['authentication']['setting_name'] ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" authentication setting_name must be a non-empty string.' ), esc_html( $id ) ), '7.0.0' ); return null; } $connector['authentication']['setting_name'] = $args['authentication']['setting_name']; } else { $connector['authentication']['setting_name'] = str_replace( '-', '_', "connectors_{$connector['type']}_{$id}_{$args['authentication']['method']}" ); } if ( isset( $args['authentication']['constant_name'] ) ) { if ( ! is_string( $args['authentication']['constant_name'] ) || '' === $args['authentication']['constant_name'] ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" authentication constant_name must be a non-empty string.' ), esc_html( $id ) ), '7.0.0' ); return null; } $connector['authentication']['constant_name'] = $args['authentication']['constant_name']; } if ( isset( $args['authentication']['env_var_name'] ) ) { if ( ! is_string( $args['authentication']['env_var_name'] ) || '' === $args['authentication']['env_var_name'] ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" authentication env_var_name must be a non-empty string.' ), esc_html( $id ) ), '7.0.0' ); return null; } $connector['authentication']['env_var_name'] = $args['authentication']['env_var_name']; } } $connector['plugin'] = array(); if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) { if ( ! empty( $args['plugin']['file'] ) ) { $connector['plugin']['file'] = $args['plugin']['file']; } if ( isset( $args['plugin']['is_active'] ) ) { if ( ! is_callable( $args['plugin']['is_active'] ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" plugin is_active must be callable.' ), esc_html( $id ) ), '7.0.0' ); return null; } $connector['plugin']['is_active'] = $args['plugin']['is_active']; } } if ( ! isset( $connector['plugin']['is_active'] ) ) { $connector['plugin']['is_active'] = '__return_true'; } $this->registered_connectors[ $id ] = $connector; return $connector; } /** * Unregisters a connector. * * Returns the connector data on success, which can be modified and passed * back to `register()` to override a connector's metadata. * * Triggers a `_doing_it_wrong()` notice if the connector is not registered. * Use `is_registered()` to check first when the connector may not exist. * * @since 7.0.0 * * @see WP_Connector_Registry::register() * @see WP_Connector_Registry::is_registered() * * @param string $id The connector identifier. * @return array|null The unregistered connector data on success, null on failure. * * @phpstan-return Connector|null */ public function unregister( string $id ): ?array { if ( ! $this->is_registered( $id ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" not found.' ), esc_html( $id ) ), '7.0.0' ); return null; } $unregistered = $this->registered_connectors[ $id ]; unset( $this->registered_connectors[ $id ] ); return $unregistered; } /** * Retrieves the list of all registered connectors. * * Do not use this method directly. Instead, use the `wp_get_connectors()` function. * * @since 7.0.0 * * @see wp_get_connectors() * * @return array Connector settings keyed by connector ID. * * @phpstan-return array */ public function get_all_registered(): array { return $this->registered_connectors; } /** * Checks if a connector is registered. * * Do not use this method directly. Instead, use the `wp_is_connector_registered()` function. * * @since 7.0.0 * * @see wp_is_connector_registered() * * @param string $id The connector identifier. * @return bool True if the connector is registered, false otherwise. */ public function is_registered( string $id ): bool { return isset( $this->registered_connectors[ $id ] ); } /** * Retrieves a registered connector. * * Do not use this method directly. Instead, use the `wp_get_connector()` function. * * Triggers a `_doing_it_wrong()` notice if the connector is not registered. * Use `is_registered()` to check first when the connector may not exist. * * @since 7.0.0 * * @see wp_get_connector() * * @param string $id The connector identifier. * @return array|null The registered connector data, or null if it is not registered. * @phpstan-return Connector|null */ public function get_registered( string $id ): ?array { if ( ! $this->is_registered( $id ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Connector ID. */ sprintf( __( 'Connector "%s" not found.' ), esc_html( $id ) ), '7.0.0' ); return null; } return $this->registered_connectors[ $id ]; } /** * Retrieves the main instance of the registry class. * * @since 7.0.0 * * @return WP_Connector_Registry|null The main registry instance, or null if not yet initialized. */ public static function get_instance(): ?self { return self::$instance; } /** * Sets the main instance of the registry class. * * Called by `_wp_connectors_init()` during the `init` action. Must not be * called outside of that context. * * @since 7.0.0 * @access private * * @see _wp_connectors_init() * * @param WP_Connector_Registry $registry The registry instance. */ public static function set_instance( WP_Connector_Registry $registry ): void { if ( ! doing_action( 'init' ) ) { _doing_it_wrong( __METHOD__, __( 'The connector registry instance must be set during the init action.' ), '7.0.0' ); return; } self::$instance = $registry; } } context['query']['postType'] ?? get_post_type(); if ( ! $type || ( 'archive' === $type && ! $is_archive ) || ( 'search' === $type && ! $is_search ) || ( 'post-type' === $type && ! $post_type ) ) { return ''; } $title = ''; if ( $is_archive ) { $show_prefix = $attributes['showPrefix'] ?? true; if ( ! $show_prefix ) { add_filter( 'get_the_archive_title_prefix', '__return_empty_string', 1 ); $title = get_the_archive_title(); remove_filter( 'get_the_archive_title_prefix', '__return_empty_string', 1 ); } else { $title = get_the_archive_title(); } } if ( $is_search ) { $title = __( 'Search results' ); if ( isset( $attributes['showSearchTerm'] ) && $attributes['showSearchTerm'] ) { $title = sprintf( /* translators: %s is the search term. */ __( 'Search results for: “%s”' ), get_search_query() ); } } if ( 'post-type' === $type ) { $post_type_object = get_post_type_object( $post_type ); if ( ! $post_type_object ) { return ''; } $post_type_name = $post_type_object->labels->singular_name; $show_prefix = $attributes['showPrefix'] ?? true; if ( $show_prefix ) { $title = sprintf( /* translators: %s is the post type name. */ __( 'Post Type: “%s”' ), $post_type_name ); } else { $title = $post_type_name; } } $level = (int) ( $attributes['level'] ?? 1 ); $tag_name = 0 === $level ? 'p' : 'h' . $level; $align_class_name = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}"; $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $align_class_name ) ); return sprintf( '<%1$s %2$s>%3$s', $tag_name, $wrapper_attributes, $title ); } /** * Registers the `core/query-title` block on the server. * * @since 5.8.0 */ function register_block_core_query_title() { register_block_type_from_metadata( __DIR__ . '/query-title', array( 'render_callback' => 'render_block_core_query_title', ) ); } add_action( 'init', 'register_block_core_query_title' ); isPaging == false) UniteFunctionsUC::throwError("The paging should be available"); } /** * validate that it's inside top actions */ private function validateTopActions(){ if($this->isInsideTopActions == false) UniteFunctionsUC::throwError("The top actions form should be started"); } private function a_GETTERS(){} /** * get page from get */ private function getPageFromGet(){ $page = UniteFunctionsUC::getGetVar(self::GET_FIELD_PAGE,1,UniteFunctionsUC::SANITIZE_ID); $page = (int)$page; return($page); } /** * get inpage from get */ private function getInPageFromGet(){ $inpage = UniteFunctionsUC::getGetVar(self::GET_FIELD_INPAGE, $this->defaultInPage, UniteFunctionsUC::SANITIZE_ID); $inpage = (int)$inpage; return($inpage); } /** * get category from get */ private function getCategoryFromGet(){ $cat = UniteFunctionsUC::getGetVar(self::GET_FIELD_CATEGORY, "", UniteFunctionsUC::SANITIZE_KEY); if($cat == "all" || $cat === "") $cat = null; else $cat = (int)$cat; return($cat); } /** * get search value from get */ private function getSearchValueFromGet(){ $search = UniteFunctionsUC::getGetVar(self::GET_FIELD_SEARCH, "", UniteFunctionsUC::SANITIZE_TEXT_FIELD); return($search); } /** * get order value from get */ private function getOrderValueFromGet(){ $order = UniteFunctionsUC::getGetVar(self::GET_FIELD_ORDER, "", UniteFunctionsUC::SANITIZE_TEXT_FIELD); if(empty($order)) $order = $this->defaultOrderby; return($order); } /** * get all available get fields */ private function getGetFieldsNames($includeBaseFields = false, $isBaseOnly = false){ $fields = array(); if($includeBaseFields == true){ $fields[] = self::GET_FIELD_OPTION; $fields[] = self::GET_FIELD_VIEW; if($isBaseOnly == true) return($fields); } $fields[] = self::GET_FIELD_PAGE; $fields[] = self::GET_FIELD_INPAGE; $fields[] = self::GET_FIELD_SEARCH; $fields[] = self::GET_FIELD_CATEGORY; $fields[] = self::GET_FIELD_ORDER; $fields[] = self::GET_FIELD_ORDER_DIR; return($fields); } /** * get field values from get, from names array */ private function getArrFieldsValuesFromGet($fieldNames, $exceptField = null){ $arrFields = array(); foreach($fieldNames as $name){ if(!empty($exceptField) && $name == $exceptField) continue; $fieldValue = UniteFunctionsUC::getGetVar($name, "", UniteFunctionsUC::SANITIZE_TEXT_FIELD); if(!empty($fieldValue)) $arrFields[$name] = $fieldValue; } return($arrFields); } /** * get array of fields from get */ private function getArrGetFields($includeBaseFields = false, $exceptField = null){ $fieldNames = $this->getGetFieldsNames($includeBaseFields); $arrFields = $this->getArrFieldsValuesFromGet($fieldNames, $exceptField); return($arrFields); } /** * get base fields obnly */ private function getArrBaseFields(){ $fieldNames = $this->getGetFieldsNames(true, true); $arrFields = $this->getArrFieldsValuesFromGet($fieldNames); return($arrFields); } /** * get page url */ private function getUrlPage($page = null,$exceptField=null){ $arrGetFields = $this->getArrGetFields(false, $exceptField); if(!empty($page)) $arrGetFields[self::GET_FIELD_PAGE] = $page; $urlPage = UniteFunctionsUC::addUrlParams($this->baseUrl, $arrGetFields); return($urlPage); } private function a_SETTERS(){} /** * set default orderby */ public function setDefaultOrderby($orderby){ $this->defaultOrderby = $orderby; } private function a_GENERAL_GET(){} /** * get paging options from get and default */ public function getPagingOptions(){ $output = array(); $output["page"] = $this->getPageFromGet(); $output["inpage"] = $this->getInPageFromGet(); $output["search"] = $this->getSearchValueFromGet(); $output["category"] = $this->getCategoryFromGet(); //take ordering $ordering = $this->getOrderValueFromGet(); $ordering = str_replace("_desc", " desc", $ordering); $output["ordering"] = $ordering; return($output); } /** * set paging data */ public function setPagingData($baseURl, $data){ $this->baseUrl = $baseURl; $this->total = UniteFunctionsUC::getVal($data, "total"); $this->page = UniteFunctionsUC::getVal($data, "page"); $this->inPage = UniteFunctionsUC::getVal($data, "inpage"); $this->numPages = UniteFunctionsUC::getVal($data, "num_pages"); UniteFunctionsUC::validateNotEmpty($this->inPage, "in page"); if($this->total > 0){ UniteFunctionsUC::validateNotEmpty($this->page, "page"); UniteFunctionsUC::validateNotEmpty($this->numPages, "num pages"); } $this->isPaging = true; } private function a_GET_HTML(){} /** * convert fields array to html hidden inputs */ private function arrFieldsToHtmlHiddenInputs($arrGetFields){ $html = ""; foreach($arrGetFields as $name=>$value) $html .= self::TAB3.HelperHtmlUC::getHiddenInputField($name, $value).self::BR; return($html); } /** * get all hidden fields html */ private function getHtmlHiddenInputs($except_field){ $arrGetFields = $this->getArrGetFields(true, $except_field); $html = $this->arrFieldsToHtmlHiddenInputs($arrGetFields); return($html); } /** * get all hidden fields html */ private function getHtmlHiddenBaseInputs(){ $arrGetFields = $this->getArrBaseFields(); $html = $this->arrFieldsToHtmlHiddenInputs($arrGetFields); return($html); } /** * put actions form end */ public function putActionsFormStart(){ $this->validatePaging(); $url = $this->baseUrl; $url = htmlspecialchars($url); $html = ""; $html .= self::TAB2."
".self::BR2; $html .= $this->getHtmlHiddenBaseInputs(); echo UniteProviderFunctionsUC::escCombinedHtml($html); $this->isInsideTopActions = true; } /** * put actions form start */ public function putActionsFormEnd(){ $this->validateTopActions(); $html = self::TAB2."
".self::BR; $this->isInsideTopActions = false; echo UniteProviderFunctionsUC::escCombinedHtml($html); } /** * get select form */ private function getHtmlFormSelect($htmlSelect, $htmlGetFields){ $html = ""; if($this->isInsideTopActions == false) $html .= "
"; $html .= $htmlSelect; if($this->isInsideTopActions == false){ $html .= $htmlGetFields; $html .= '
'; } return $html; } /** * get input with count content, about 10,25,50,100 */ public function getHTMLInpageSelect(){ $inpage = $this->getInPageFromGet(); $arrNumbers = array( "10","25","50","100" ); $fieldInpage = self::GET_FIELD_INPAGE; $htmlSelect = HelperHtmlUC::getHTMLSelect($arrNumbers, $inpage, "name='{$fieldInpage}' class='unite-tableitems-selectrecords' onchange='this.form.submit()'"); $htmlGetFields = $this->getHtmlHiddenInputs($fieldInpage); $html = $this->getHtmlFormSelect($htmlSelect, $htmlGetFields); return($html); } /** * put filter category input */ public function putFilterCategory(){ $cat = $this->getCategoryFromGet(); $objCats = new UniteCreatorCategories; $arrCats = $objCats->getCatsShort("all_uncat_layouts", "layout"); $html = ""; $fieldCat = self::GET_FIELD_CATEGORY; if($cat === "" || $cat === null) $cat = "all"; $htmlSelect = HelperHtmlUC::getHTMLSelect($arrCats, $cat, "name='{$fieldCat}' class='unite-tableitems-category' onchange='this.form.submit()'", true); $htmlGetFields = $this->getHtmlHiddenInputs($fieldCat); $html = "".esc_html__("Filter Category", "unlimited-elements-for-elementor").""; $html .= $this->getHtmlFormSelect($htmlSelect, $htmlGetFields); echo UniteProviderFunctionsUC::escCombinedHtml($html); } /** * get pagination html */ private function getPaginationHtml(){ $this->validatePaging(); $item_per_page = $this->inPage; $current_page = $this->page; $total_records = $this->total; $total_pages = $this->numPages; $isShowExtras = true; $pagination = ''; if($total_pages > 0 && $total_pages != 1 && $current_page <= $total_pages){ //verify total pages and current page number $pagination .= ''; } return($pagination); } /** * draw table pagination */ public function putPaginationHtml(){ $this->validatePaging(); $html = $this->getPaginationHtml(); echo UniteProviderFunctionsUC::escCombinedHtml($html); } /** * put inpage select */ public function putInpageSelect(){ $this->validatePaging(); if($this->total <= 10) return(""); $html = $this->getHTMLInpageSelect(); echo UniteProviderFunctionsUC::escCombinedHtml($html); } /** * function for search content and sorting */ public function putSearchForm($buttonText = "", $clearText = "", $putByDefault = true){ //the button must be inside top actions $this->validateTopActions(); $html = ""; $fieldValue = $this->getSearchValueFromGet(); $fieldValue = htmlspecialchars($fieldValue); if(empty($buttonText)) $buttonText = esc_html__("Search", "unlimited-elements-for-elementor"); if(empty($clearText)) $clearText = esc_html__("Clear", "unlimited-elements-for-elementor"); $fieldName = self::GET_FIELD_SEARCH; $htmlFields = $this->getHtmlHiddenInputs($fieldName); $urlClear = $this->getUrlPage(); //is total allow to put search $isTotalAllow = ($this->total > 5); if($isTotalAllow == false && empty($fieldValue)) return(false); if($putByDefault == false && empty($fieldValue)) return(false); if($this->isInsideTopActions == false){ $urlForm = $this->baseUrl; $html .= self::TAB2."
".self::BR; } $html .= self::TAB2." ".self::BR; $html .= self::TAB2." ".self::BR; //add clear button if(!empty($fieldValue)){ $urlClear = $this->getUrlPage(null, self::GET_FIELD_SEARCH); $html .= self::TAB2." ". $clearText."".self::BR; } if($this->isInsideTopActions == false){ $html .= self::TAB3.$htmlFields.self::BR; $html .= self::TAB2."
".self::BR; } if(!empty($searchValue)) $html .= " ".$clearText.""; echo UniteProviderFunctionsUC::escCombinedHtml($html); } /** * put table order header - inside table th */ public function putTableOrderHeader($name, $text){ $currentOrder = $this->getOrderValueFromGet(); $currentOrderField = str_replace("_desc", "", $currentOrder); $isDesc = ($currentOrder != $currentOrderField); $orderForLink = $name; if($currentOrderField == $name && $isDesc == false) $orderForLink = $name."_desc"; $link = $this->getUrlPage(null, self::GET_FIELD_ORDER); $link = UniteFunctionsUC::addUrlParams($link, self::GET_FIELD_ORDER."=".$orderForLink); //get text $signUp = "∧"; $signDown = "∨"; $linkText = $text; if($name == $currentOrderField){ if($isDesc == true) $linkText .= " ".$signUp; else $linkText .= " ".$signDown; } $html = "$linkText"; echo UniteProviderFunctionsUC::escCombinedHtml($html); } }created_time = microtime( true ); $this->store = $store ? $store : ActionScheduler_Store::instance(); $this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store ); $this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store ); } /** * Process an individual action. * * @param int $action_id The action ID to process. * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. */ public function process_action( $action_id, $context = '' ) { // Temporarily override the error handler while we process the current action. set_error_handler( /** * Temporary error handler which can catch errors and convert them into exceptions. This faciliates more * robust error handling across all supported PHP versions. * * @throws Exception * * @param int $type Error level expressed as an integer. * @param string $message Error message. */ function ( $type, $message ) { throw new Exception( $message ); }, E_USER_ERROR | E_RECOVERABLE_ERROR ); /* * The nested try/catch structure is required because we potentially need to convert thrown errors into * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same* * structure). */ try { try { $valid_action = false; do_action( 'action_scheduler_before_execute', $action_id, $context ); if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { do_action( 'action_scheduler_execution_ignored', $action_id, $context ); return; } $valid_action = true; do_action( 'action_scheduler_begin_execute', $action_id, $context ); $action = $this->store->fetch_action( $action_id ); $this->store->log_execution( $action_id ); $action->execute(); do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); $this->store->mark_complete( $action_id ); } catch ( Throwable $e ) { // Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for // compatibility with ActionScheduler_Logger. throw new Exception( $e->getMessage(), $e->getCode(), $e ); } } catch ( Exception $e ) { // This catch block exists for compatibility with PHP 5.6. $this->handle_action_error( $action_id, $e, $context, $valid_action ); } finally { restore_error_handler(); } if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { $this->schedule_next_instance( $action, $action_id ); } } /** * Marks actions as either having failed execution or failed validation, as appropriate. * * @param int $action_id Action ID. * @param Exception $e Exception instance. * @param string $context Execution context. * @param bool $valid_action If the action is valid. * * @return void */ private function handle_action_error( $action_id, $e, $context, $valid_action ) { if ( $valid_action ) { $this->store->mark_failure( $action_id ); /** * Runs when action execution fails. * * @param int $action_id Action ID. * @param Exception $e Exception instance. * @param string $context Execution context. */ do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); } else { /** * Runs when action validation fails. * * @param int $action_id Action ID. * @param Exception $e Exception instance. * @param string $context Execution context. */ do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); } } /** * Schedule the next instance of the action if necessary. * * @param ActionScheduler_Action $action * @param int $action_id */ protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) { // If a recurring action has been consistently failing, we may wish to stop rescheduling it. if ( ActionScheduler_Store::STATUS_FAILED === $this->store->get_status( $action_id ) && $this->recurring_action_is_consistently_failing( $action, $action_id ) ) { ActionScheduler_Logger::instance()->log( $action_id, __( 'This action appears to be consistently failing. A new instance will not be scheduled.', 'woocommerce' ) ); return; } try { ActionScheduler::factory()->repeat( $action ); } catch ( Exception $e ) { do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action ); } } /** * Determine if the specified recurring action has been consistently failing. * * @param ActionScheduler_Action $action The recurring action to be rescheduled. * @param int $action_id The ID of the recurring action. * * @return bool */ private function recurring_action_is_consistently_failing( ActionScheduler_Action $action, $action_id ) { /** * Controls the failure threshold for recurring actions. * * Before rescheduling a recurring action, we look at its status. If it failed, we then check if all of the most * recent actions (upto the threshold set by this filter) sharing the same hook have also failed: if they have, * that is considered consistent failure and a new instance of the action will not be scheduled. * * @param int $failure_threshold Number of actions of the same hook to examine for failure. Defaults to 5. */ $consistent_failure_threshold = (int) apply_filters( 'action_scheduler_recurring_action_failure_threshold', 5 ); // This query should find the earliest *failing* action (for the hook we are interested in) within our threshold. $query_args = array( 'hook' => $action->get_hook(), 'status' => ActionScheduler_Store::STATUS_FAILED, 'date' => date_create( 'now', timezone_open( 'UTC' ) )->format( 'Y-m-d H:i:s' ), 'date_compare' => '<', 'per_page' => 1, 'offset' => $consistent_failure_threshold - 1 ); $first_failing_action_id = $this->store->query_actions( $query_args ); // If we didn't retrieve an action ID, then there haven't been enough failures for us to worry about. if ( empty( $first_failing_action_id ) ) { return false; } // Now let's fetch the first action (having the same hook) of *any status* within the same window. unset( $query_args['status'] ); $first_action_id_with_the_same_hook = $this->store->query_actions( $query_args ); /** * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a * way to observe and optionally override that assessment. * * @param bool $is_consistently_failing If the action is considered to be consistently failing. * @param ActionScheduler_Action $action The action being assessed. */ return (bool) apply_filters( 'action_scheduler_recurring_action_is_consistently_failing', $first_action_id_with_the_same_hook === $first_failing_action_id, $action ); } /** * Run the queue cleaner. * * @author Jeremy Pry */ protected function run_cleanup() { $this->cleaner->clean( 10 * $this->get_time_limit() ); } /** * Get the number of concurrent batches a runner allows. * * @return int */ public function get_allowed_concurrent_batches() { return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 ); } /** * Check if the number of allowed concurrent batches is met or exceeded. * * @return bool */ public function has_maximum_concurrent_batches() { return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches(); } /** * Get the maximum number of seconds a batch can run for. * * @return int The number of seconds. */ protected function get_time_limit() { $time_limit = 30; // Apply deprecated filter from deprecated get_maximum_execution_time() method if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); $time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit ); } return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) ); } /** * Get the number of seconds the process has been running. * * @return int The number of seconds. */ protected function get_execution_time() { $execution_time = microtime( true ) - $this->created_time; // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time. if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) { $resource_usages = getrusage(); if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) { $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 ); } } return $execution_time; } /** * Check if the host's max execution time is (likely) to be exceeded if processing more actions. * * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action * @return bool */ protected function time_likely_to_be_exceeded( $processed_actions ) { $execution_time = $this->get_execution_time(); $max_execution_time = $this->get_time_limit(); // Safety against division by zero errors. if ( 0 === $processed_actions ) { return $execution_time >= $max_execution_time; } $time_per_action = $execution_time / $processed_actions; $estimated_time = $execution_time + ( $time_per_action * 3 ); $likely_to_be_exceeded = $estimated_time > $max_execution_time; return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time ); } /** * Get memory limit * * Based on WP_Background_Process::get_memory_limit() * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { $memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce } if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) { // Unlimited, set to 32GB. $memory_limit = '32G'; } return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit ); } /** * Memory exceeded * * Ensures the batch process never exceeds 90% of the maximum WordPress memory. * * Based on WP_Background_Process::memory_exceeded() * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.90; $current_memory = memory_get_usage( true ); $memory_exceeded = $current_memory >= $memory_limit; return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this ); } /** * See if the batch limits have been exceeded, which is when memory usage is almost at * the maximum limit, or the time to process more actions will exceed the max time limit. * * Based on WC_Background_Process::batch_limits_exceeded() * * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action * @return bool */ protected function batch_limits_exceeded( $processed_actions ) { return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions ); } /** * Process actions in the queue. * * @author Jeremy Pry * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ abstract public function run( $context = '' ); }